Rewrite Cargo.toml when packaging crates
authorAlex Crichton <alex@alexcrichton.com>
Thu, 11 May 2017 05:09:44 +0000 (22:09 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Wed, 17 May 2017 16:23:53 +0000 (09:23 -0700)
This commit is an implementation of rewriting TOML manifests when we publish
them to the registry. The rationale for doing this is to provide a guarantee
that downloaded tarballs from crates.io can be built with `cargo build`
(literally). This in turn eases a number of other possible consumers of crates
from crates.io

* Vendored sources can now be more easily modified/checked as cargo build should
  work and they're standalone crates that suffice for `path` dependencies
* Tools like cargobomb/crater no longer need to edit the manifest and can
  instead perform regression testing on the literal tarballs they download
* Other systems such as packaging Rust code may be able to take advantage of
  this, but this is a less clear benefit.

Overall I'm hesitatnt about this, unfortunately. This is a silent translation
happening on *publish*, a rare operation, that's difficult to inspect before it
flies up to crates.io. I wrote a script to run this transformation over all
crates.io crates and found a surprisingly large number of discrepancies. The
transformation basically just downloaded all crates at all versions,
regenerated the manifest, and then tested if the two manifests were (in memory)
the same.

Unfortunately historical Cargo had a critical bug which I think made this
exercise not too useful. Cargo used to *not* recreate tarballs if one already
existed, which I believe led to situations such as:

1. `cargo publish`
2. Cargo generates an error about a dependency. This could be that there's a
   `version` not present in a `path` dependency, there could be a `git`
   dependency, etc.
3. Errors are fixed.
4. `cargo publish`
5. Publish is successful

In step 4 above historical Cargo *would not recreate the tarball*. This means
that the contents of the index (what was published) aren't guaranteed to match
with the tarball's `Cargo.toml`. When building from crates.io this is ok as the
index is the source of truth for dependency information, but it means that *any*
transformation to rewrite Cargo.toml is impossible to verify against all crates
on crates.io (due to historical bugs like these).

I strove to read as many errors as possible regardless, attempting to suss out
bugs in the implementation here. To further guard against surprises I've updated
the verification step of packaging to work "normally" in these sense that it's
not rewriting dependencies itself or changing summaries. I'm hoping that this
serves as a good last-ditch effort that what we're about to publish will indeed
build as expected when uploaded to crates.io

Overall I'm probably 70% confident in this change. I think it's necessary to
make progress, but I think there are going to be very painful bugs that arise
from this feature. I'm open to ideas to help weed out these bugs ahead of time!
I've done what I can but I fear it may not be entirely enough.

Closes #4027

src/cargo/core/manifest.rs
src/cargo/core/package.rs
src/cargo/core/workspace.rs
src/cargo/ops/cargo_package.rs
src/cargo/ops/cargo_rustc/context.rs
src/cargo/util/toml.rs
tests/package.rs
tests/publish.rs

index e0b8d36016d96900df45c472272ac837028a501f..14087106492020e3003f49b05767572440183fca 100644 (file)
@@ -1,12 +1,14 @@
 use std::collections::HashMap;
 use std::fmt;
 use std::path::{PathBuf, Path};
+use std::rc::Rc;
 
 use semver::Version;
 use serde::ser;
 
 use core::{Dependency, PackageId, Summary, SourceId, PackageIdSpec};
 use core::WorkspaceConfig;
+use util::toml::TomlManifest;
 
 pub enum EitherManifest {
     Real(Manifest),
@@ -14,7 +16,7 @@ pub enum EitherManifest {
 }
 
 /// Contains all the information about a package, as loaded from a Cargo.toml.
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct Manifest {
     summary: Summary,
     targets: Vec<Target>,
@@ -27,6 +29,7 @@ pub struct Manifest {
     publish: bool,
     replace: Vec<(PackageIdSpec, Dependency)>,
     workspace: WorkspaceConfig,
+    original: Rc<TomlManifest>,
 }
 
 #[derive(Clone, Debug)]
@@ -222,7 +225,8 @@ impl Manifest {
                profiles: Profiles,
                publish: bool,
                replace: Vec<(PackageIdSpec, Dependency)>,
-               workspace: WorkspaceConfig) -> Manifest {
+               workspace: WorkspaceConfig,
+               original: Rc<TomlManifest>) -> Manifest {
         Manifest {
             summary: summary,
             targets: targets,
@@ -235,6 +239,7 @@ impl Manifest {
             publish: publish,
             replace: replace,
             workspace: workspace,
+            original: original,
         }
     }
 
@@ -251,6 +256,7 @@ impl Manifest {
     pub fn profiles(&self) -> &Profiles { &self.profiles }
     pub fn publish(&self) -> bool { self.publish }
     pub fn replace(&self) -> &[(PackageIdSpec, Dependency)] { &self.replace }
+    pub fn original(&self) -> &TomlManifest { &self.original }
     pub fn links(&self) -> Option<&str> {
         self.links.as_ref().map(|s| &s[..])
     }
index 898745460f98abc08560af0b534dd779ea72c567..943f570d335c9cdb15d6eca4168bafc2dd384a66 100644 (file)
@@ -6,6 +6,7 @@ use std::path::{Path, PathBuf};
 
 use semver::Version;
 use serde::ser;
+use toml;
 
 use core::{Dependency, Manifest, PackageId, SourceId, Target};
 use core::{Summary, SourceMap};
@@ -16,7 +17,7 @@ use util::{CargoResult, Config, LazyCell, ChainError, internal, human, lev_dista
 ///
 /// A package is a `Cargo.toml` file plus all the files that are part of it.
 // TODO: Is manifest_path a relic?
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 pub struct Package {
     // The package's manifest
     manifest: Manifest,
@@ -117,6 +118,26 @@ impl Package {
             manifest_path: self.manifest_path,
         }
     }
+
+    pub fn to_registry_toml(&self) -> String {
+        let manifest = self.manifest().original().prepare_for_publish();
+        let toml = toml::to_string(&manifest).unwrap();
+        format!("\
+            # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO\n\
+            #\n\
+            # When uploading crates to the registry Cargo will automatically\n\
+            # \"normalize\" Cargo.toml files for maximal compatibility\n\
+            # with all versions of Cargo and also rewrite `path` dependencies\n\
+            # to registry (e.g. crates.io) dependencies\n\
+            #\n\
+            # If you believe there's an error in this file please file an\n\
+            # issue against the rust-lang/cargo repository. If you're\n\
+            # editing this file be aware that the upstream Cargo.toml\n\
+            # will likely look very different (and much more reasonable)\n\
+            \n\
+            {}\
+        ", toml)
+    }
 }
 
 impl fmt::Display for Package {
index 5015448a569cef6494b82825e2ef72fa836ab076..0607e828bcd84c4155e1cf5cc267565a7df2f82b 100644 (file)
@@ -124,7 +124,9 @@ impl<'cfg> Workspace<'cfg> {
     ///
     /// This is currently only used in niche situations like `cargo install` or
     /// `cargo package`.
-    pub fn ephemeral(package: Package, config: &'cfg Config, target_dir: Option<Filesystem>,
+    pub fn ephemeral(package: Package,
+                     config: &'cfg Config,
+                     target_dir: Option<Filesystem>,
                      require_optional_deps: bool) -> CargoResult<Workspace<'cfg>> {
         let mut ws = Workspace {
             config: config,
index 107a4f2a55462f13e02a6aa66e9312d18946da98..de788bab5b60e2e558b11e0050762dbaf36b07b8 100644 (file)
@@ -7,9 +7,9 @@ use std::sync::Arc;
 use flate2::read::GzDecoder;
 use flate2::{GzBuilder, Compression};
 use git2;
-use tar::{Archive, Builder, Header};
+use tar::{Archive, Builder, Header, EntryType};
 
-use core::{SourceId, Package, PackageId, Workspace, Source};
+use core::{Package, Workspace, Source, SourceId};
 use sources::PathSource;
 use util::{self, CargoResult, human, internal, ChainError, Config, FileLock};
 use ops::{self, DefaultExecutor};
@@ -202,9 +202,6 @@ fn tar(ws: &Workspace,
             human(format!("non-utf8 path in source directory: {}",
                           relative.display()))
         })?;
-        let mut file = File::open(file).chain_error(|| {
-            human(format!("failed to open for archiving: `{}`", file.display()))
-        })?;
         config.shell().verbose(|shell| {
             shell.status("Archiving", &relative)
         })?;
@@ -230,18 +227,41 @@ fn tar(ws: &Workspace,
         // unpack the selectors 0.4.0 crate on crates.io. Either that or take a
         // look at rust-lang/cargo#2326
         let mut header = Header::new_ustar();
-        let metadata = file.metadata().chain_error(|| {
-            human(format!("could not learn metadata for: `{}`", relative))
-        })?;
         header.set_path(&path).chain_error(|| {
             human(format!("failed to add to archive: `{}`", relative))
         })?;
+        let mut file = File::open(file).chain_error(|| {
+            human(format!("failed to open for archiving: `{}`", file.display()))
+        })?;
+        let metadata = file.metadata().chain_error(|| {
+            human(format!("could not learn metadata for: `{}`", relative))
+        })?;
         header.set_metadata(&metadata);
-        header.set_cksum();
 
-        ar.append(&header, &mut file).chain_error(|| {
-            internal(format!("could not archive source file `{}`", relative))
-        })?;
+        if relative == "Cargo.toml" {
+            let orig = Path::new(&path).with_file_name("Cargo.toml.orig");
+            header.set_path(&orig)?;
+            header.set_cksum();
+            ar.append(&header, &mut file).chain_error(|| {
+                internal(format!("could not archive source file `{}`", relative))
+            })?;
+
+            let mut header = Header::new_ustar();
+            let toml = pkg.to_registry_toml();
+            header.set_path(&path)?;
+            header.set_entry_type(EntryType::file());
+            header.set_mode(0o644);
+            header.set_size(toml.len() as u64);
+            header.set_cksum();
+            ar.append(&header, toml.as_bytes()).chain_error(|| {
+                internal(format!("could not archive source file `{}`", relative))
+            })?;
+        } else {
+            header.set_cksum();
+            ar.append(&header, &mut file).chain_error(|| {
+                internal(format!("could not archive source file `{}`", relative))
+            })?;
+        }
     }
     let encoder = ar.into_inner()?;
     encoder.finish()?;
@@ -262,30 +282,14 @@ fn run_verify(ws: &Workspace, tar: &File, opts: &PackageOpts) -> CargoResult<()>
     }
     let mut archive = Archive::new(f);
     archive.unpack(dst.parent().unwrap())?;
-    let manifest_path = dst.join("Cargo.toml");
 
-    // When packages are uploaded to a registry, all path dependencies are
-    // implicitly converted to registry dependencies, so we rewrite those
-    // dependencies here.
-    //
-    // We also make sure to point all paths at `dst` instead of the previous
-    // location that the package was originally read from. In locking the
-    // `SourceId` we're telling it that the corresponding `PathSource` will be
-    // considered updated and we won't actually read any packages.
-    let cratesio = SourceId::crates_io(config)?;
-    let precise = Some("locked".to_string());
-    let new_src = SourceId::for_path(&dst)?.with_precise(precise);
-    let new_pkgid = PackageId::new(pkg.name(), pkg.version(), &new_src)?;
-    let new_summary = pkg.summary().clone().map_dependencies(|d| {
-        if !d.source_id().is_path() { return d }
-        d.clone_inner().set_source_id(cratesio.clone()).into_dependency()
-    });
-    let mut new_manifest = pkg.manifest().clone();
-    new_manifest.set_summary(new_summary.override_id(new_pkgid));
-    let new_pkg = Package::new(new_manifest, &manifest_path);
-
-    // Now that we've rewritten all our path dependencies, compile it!
+    // Manufacture an ephemeral workspace to ensure that even if the top-level
+    // package has a workspace we can still build our new crate.
+    let id = SourceId::for_path(&dst)?;
+    let mut src = PathSource::new(&dst, &id, ws.config());
+    let new_pkg = src.root_package()?;
     let ws = Workspace::ephemeral(new_pkg, config, None, true)?;
+
     ops::compile_ws(&ws, None, &ops::CompileOptions {
         config: config,
         jobs: opts.jobs,
index e17a0823b6c0e220098bab5b6eecccfa48970dd9..34d9c8cc9b6d45397875db296a77257ae6f93af6 100644 (file)
@@ -20,7 +20,7 @@ use super::layout::Layout;
 use super::links::Links;
 use super::{Kind, Compilation, BuildConfig};
 
-#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
+#[derive(Clone, Copy, Eq, PartialEq, Hash)]
 pub struct Unit<'a> {
     pub pkg: &'a Package,
     pub target: &'a Target,
index 771f48ddfda49e720db4cda8a7c52cfc516aef52..254d03280171276db411370b149f8ef7c08fae3a 100644 (file)
@@ -1,12 +1,13 @@
 use std::collections::{HashMap, HashSet, BTreeSet};
-use std::default::Default;
 use std::fmt;
 use std::fs;
 use std::path::{Path, PathBuf};
+use std::rc::Rc;
 use std::str;
 
 use toml;
 use semver::{self, VersionReq};
+use serde::ser;
 use serde::de::{self, Deserialize};
 use serde_ignored;
 
@@ -112,7 +113,11 @@ pub fn to_manifest(contents: &str,
         }
     })?;
 
-    return match manifest.to_real_manifest(source_id, &layout, config) {
+    let manifest = Rc::new(manifest);
+    return match TomlManifest::to_real_manifest(&manifest,
+                                                source_id,
+                                                &layout,
+                                                config) {
         Ok((mut manifest, paths)) => {
             for key in unused {
                 manifest.add_warning(format!("unused manifest key: {}", key));
@@ -125,7 +130,10 @@ pub fn to_manifest(contents: &str,
             Ok((EitherManifest::Real(manifest), paths))
         }
         Err(e) => {
-            match manifest.to_virtual_manifest(source_id, &layout, config) {
+            match TomlManifest::to_virtual_manifest(&manifest,
+                                                    source_id,
+                                                    &layout,
+                                                    config) {
                 Ok((m, paths)) => Ok((EitherManifest::Virtual(m), paths)),
                 Err(..) => Err(e),
             }
@@ -192,6 +200,8 @@ type TomlExampleTarget = TomlTarget;
 type TomlTestTarget = TomlTarget;
 type TomlBenchTarget = TomlTarget;
 
+#[derive(Serialize)]
+#[serde(untagged)]
 pub enum TomlDependency {
     Simple(String),
     Detailed(DetailedTomlDependency)
@@ -229,7 +239,7 @@ impl<'de> de::Deserialize<'de> for TomlDependency {
     }
 }
 
-#[derive(Deserialize, Clone, Default)]
+#[derive(Deserialize, Serialize, Clone, Default)]
 pub struct DetailedTomlDependency {
     version: Option<String>,
     path: Option<String>,
@@ -245,7 +255,7 @@ pub struct DetailedTomlDependency {
     default_features2: Option<bool>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Serialize)]
 pub struct TomlManifest {
     package: Option<Box<TomlProject>>,
     project: Option<Box<TomlProject>>,
@@ -271,7 +281,7 @@ pub struct TomlManifest {
     badges: Option<HashMap<String, HashMap<String, String>>>,
 }
 
-#[derive(Deserialize, Clone, Default)]
+#[derive(Deserialize, Serialize, Clone, Default)]
 pub struct TomlProfiles {
     test: Option<TomlProfile>,
     doc: Option<TomlProfile>,
@@ -318,7 +328,19 @@ impl<'de> de::Deserialize<'de> for TomlOptLevel {
     }
 }
 
-#[derive(Clone)]
+impl ser::Serialize for TomlOptLevel {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where S: ser::Serializer,
+    {
+        match self.0.parse::<u32>() {
+            Ok(n) => n.serialize(serializer),
+            Err(_) => self.0.serialize(serializer),
+        }
+    }
+}
+
+#[derive(Clone, Serialize)]
+#[serde(untagged)]
 pub enum U32OrBool {
     U32(u32),
     Bool(bool),
@@ -360,7 +382,7 @@ impl<'de> de::Deserialize<'de> for U32OrBool {
     }
 }
 
-#[derive(Deserialize, Clone, Default)]
+#[derive(Deserialize, Serialize, Clone, Default)]
 pub struct TomlProfile {
     #[serde(rename = "opt-level")]
     opt_level: Option<TomlOptLevel>,
@@ -376,7 +398,8 @@ pub struct TomlProfile {
     overflow_checks: Option<bool>,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize)]
+#[serde(untagged)]
 pub enum StringOrBool {
     String(String),
     Bool(bool),
@@ -412,7 +435,7 @@ impl<'de> de::Deserialize<'de> for StringOrBool {
     }
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Serialize, Clone)]
 pub struct TomlProject {
     name: String,
     version: TomlVersion,
@@ -437,12 +460,13 @@ pub struct TomlProject {
     repository: Option<String>,
 }
 
-#[derive(Deserialize)]
+#[derive(Deserialize, Serialize)]
 pub struct TomlWorkspace {
     members: Option<Vec<String>>,
     exclude: Option<Vec<String>>,
 }
 
+#[derive(Clone)]
 pub struct TomlVersion {
     version: semver::Version,
 }
@@ -474,6 +498,15 @@ impl<'de> de::Deserialize<'de> for TomlVersion {
     }
 }
 
+impl ser::Serialize for TomlVersion {
+    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
+        where S: ser::Serializer,
+    {
+        self.version.to_string().serialize(s)
+    }
+}
+
+
 impl TomlProject {
     pub fn to_package_id(&self, source_id: &SourceId) -> CargoResult<PackageId> {
         PackageId::new(&self.name, self.version.version.clone(),
@@ -564,7 +597,72 @@ fn inferred_bench_targets(layout: &Layout) -> Vec<TomlTarget> {
 }
 
 impl TomlManifest {
-    fn to_real_manifest(&self,
+    pub fn prepare_for_publish(&self) -> TomlManifest {
+        let mut package = self.package.as_ref()
+                              .or(self.project.as_ref())
+                              .unwrap()
+                              .clone();
+        package.workspace = None;
+        return TomlManifest {
+            package: Some(package),
+            project: None,
+            profile: self.profile.clone(),
+            lib: self.lib.clone(),
+            bin: self.bin.clone(),
+            example: self.example.clone(),
+            test: self.test.clone(),
+            bench: self.bench.clone(),
+            dependencies: map_deps(self.dependencies.as_ref()),
+            dev_dependencies: map_deps(self.dev_dependencies.as_ref()
+                                         .or(self.dev_dependencies2.as_ref())),
+            dev_dependencies2: None,
+            build_dependencies: map_deps(self.build_dependencies.as_ref()
+                                         .or(self.build_dependencies2.as_ref())),
+            build_dependencies2: None,
+            features: self.features.clone(),
+            target: self.target.as_ref().map(|target_map| {
+                target_map.iter().map(|(k, v)| {
+                    (k.clone(), TomlPlatform {
+                        dependencies: map_deps(v.dependencies.as_ref()),
+                        dev_dependencies: map_deps(v.dev_dependencies.as_ref()
+                                                     .or(v.dev_dependencies2.as_ref())),
+                        dev_dependencies2: None,
+                        build_dependencies: map_deps(v.build_dependencies.as_ref()
+                                                     .or(v.build_dependencies2.as_ref())),
+                        build_dependencies2: None,
+                    })
+                }).collect()
+            }),
+            replace: None,
+            workspace: None,
+            badges: self.badges.clone(),
+        };
+
+        fn map_deps(deps: Option<&HashMap<String, TomlDependency>>)
+                        -> Option<HashMap<String, TomlDependency>>
+        {
+            let deps = match deps {
+                Some(deps) => deps,
+                None => return None
+            };
+            Some(deps.iter().map(|(k, v)| (k.clone(), map_dependency(v))).collect())
+        }
+
+        fn map_dependency(dep: &TomlDependency) -> TomlDependency {
+            match *dep {
+                TomlDependency::Detailed(ref d) => {
+                    let mut d = d.clone();
+                    d.path.take(); // path dependencies become crates.io deps
+                    TomlDependency::Detailed(d)
+                }
+                TomlDependency::Simple(ref s) => {
+                    TomlDependency::Simple(s.clone())
+                }
+            }
+        }
+    }
+
+    fn to_real_manifest(me: &Rc<TomlManifest>,
                         source_id: &SourceId,
                         layout: &Layout,
                         config: &Config)
@@ -572,7 +670,7 @@ impl TomlManifest {
         let mut nested_paths = vec![];
         let mut warnings = vec![];
 
-        let project = self.project.as_ref().or_else(|| self.package.as_ref());
+        let project = me.project.as_ref().or_else(|| me.package.as_ref());
         let project = project.chain_error(|| {
             human("no `package` or `project` section found.")
         })?;
@@ -587,7 +685,7 @@ impl TomlManifest {
         // If we have a lib with a path, we're done
         // If we have a lib with no path, use the inferred lib or_else package name
 
-        let lib = match self.lib {
+        let lib = match me.lib {
             Some(ref lib) => {
                 lib.validate_library_name()?;
                 lib.validate_crate_type()?;
@@ -604,7 +702,7 @@ impl TomlManifest {
             None => inferred_lib_target(&project.name, layout),
         };
 
-        let bins = match self.bin {
+        let bins = match me.bin {
             Some(ref bins) => {
                 for target in bins {
                     target.validate_binary_name()?;
@@ -621,7 +719,7 @@ impl TomlManifest {
             }
         }
 
-        let examples = match self.example {
+        let examples = match me.example {
             Some(ref examples) => {
                 for target in examples {
                     target.validate_example_name()?;
@@ -631,7 +729,7 @@ impl TomlManifest {
             None => inferred_example_targets(layout)
         };
 
-        let tests = match self.test {
+        let tests = match me.test {
             Some(ref tests) => {
                 for target in tests {
                     target.validate_test_name()?;
@@ -641,7 +739,7 @@ impl TomlManifest {
             None => inferred_test_targets(layout)
         };
 
-        let benches = match self.bench {
+        let benches = match me.bench {
             Some(ref benches) => {
                 for target in benches {
                     target.validate_bench_name()?;
@@ -672,7 +770,7 @@ impl TomlManifest {
         }
 
         // processing the custom build script
-        let new_build = self.maybe_custom_build(&project.build, &layout.root);
+        let new_build = me.maybe_custom_build(&project.build, &layout.root);
 
         // Get targets
         let targets = normalize(&layout.root,
@@ -727,16 +825,16 @@ impl TomlManifest {
             }
 
             // Collect the deps
-            process_dependencies(&mut cx, self.dependencies.as_ref(),
+            process_dependencies(&mut cx, me.dependencies.as_ref(),
                                  None)?;
-            let dev_deps = self.dev_dependencies.as_ref()
-                               .or(self.dev_dependencies2.as_ref());
+            let dev_deps = me.dev_dependencies.as_ref()
+                               .or(me.dev_dependencies2.as_ref());
             process_dependencies(&mut cx, dev_deps, Some(Kind::Development))?;
-            let build_deps = self.build_dependencies.as_ref()
-                               .or(self.build_dependencies2.as_ref());
+            let build_deps = me.build_dependencies.as_ref()
+                               .or(me.build_dependencies2.as_ref());
             process_dependencies(&mut cx, build_deps, Some(Kind::Build))?;
 
-            for (name, platform) in self.target.iter().flat_map(|t| t) {
+            for (name, platform) in me.target.iter().flat_map(|t| t) {
                 cx.platform = Some(name.parse()?);
                 process_dependencies(&mut cx, platform.dependencies.as_ref(),
                                      None)?;
@@ -748,7 +846,7 @@ impl TomlManifest {
                 process_dependencies(&mut cx, dev_deps, Some(Kind::Development))?;
             }
 
-            replace = self.replace(&mut cx)?;
+            replace = me.replace(&mut cx)?;
         }
 
         {
@@ -767,7 +865,7 @@ impl TomlManifest {
         let exclude = project.exclude.clone().unwrap_or(Vec::new());
         let include = project.include.clone().unwrap_or(Vec::new());
 
-        let summary = Summary::new(pkgid, deps, self.features.clone()
+        let summary = Summary::new(pkgid, deps, me.features.clone()
             .unwrap_or_else(HashMap::new))?;
         let metadata = ManifestMetadata {
             description: project.description.clone(),
@@ -780,10 +878,10 @@ impl TomlManifest {
             repository: project.repository.clone(),
             keywords: project.keywords.clone().unwrap_or(Vec::new()),
             categories: project.categories.clone().unwrap_or(Vec::new()),
-            badges: self.badges.clone().unwrap_or_else(HashMap::new),
+            badges: me.badges.clone().unwrap_or_else(HashMap::new),
         };
 
-        let workspace_config = match (self.workspace.as_ref(),
+        let workspace_config = match (me.workspace.as_ref(),
                                       project.workspace.as_ref()) {
             (Some(config), None) => {
                 WorkspaceConfig::Root {
@@ -799,7 +897,7 @@ impl TomlManifest {
                        `[workspace]`, only one can be specified")
             }
         };
-        let profiles = build_profiles(&self.profile);
+        let profiles = build_profiles(&me.profile);
         let publish = project.publish.unwrap_or(true);
         let mut manifest = Manifest::new(summary,
                                          targets,
@@ -810,7 +908,8 @@ impl TomlManifest {
                                          profiles,
                                          publish,
                                          replace,
-                                         workspace_config);
+                                         workspace_config,
+                                         me.clone());
         if project.license_file.is_some() && project.license.is_some() {
             manifest.add_warning("only one of `license` or \
                                  `license-file` is necessary".to_string());
@@ -822,37 +921,37 @@ impl TomlManifest {
         Ok((manifest, nested_paths))
     }
 
-    fn to_virtual_manifest(&self,
+    fn to_virtual_manifest(me: &Rc<TomlManifest>,
                            source_id: &SourceId,
                            layout: &Layout,
                            config: &Config)
                            -> CargoResult<(VirtualManifest, Vec<PathBuf>)> {
-        if self.project.is_some() {
+        if me.project.is_some() {
             bail!("virtual manifests do not define [project]");
         }
-        if self.package.is_some() {
+        if me.package.is_some() {
             bail!("virtual manifests do not define [package]");
         }
-        if self.lib.is_some() {
+        if me.lib.is_some() {
             bail!("virtual manifests do not specifiy [lib]");
         }
-        if self.bin.is_some() {
+        if me.bin.is_some() {
             bail!("virtual manifests do not specifiy [[bin]]");
         }
-        if self.example.is_some() {
+        if me.example.is_some() {
             bail!("virtual manifests do not specifiy [[example]]");
         }
-        if self.test.is_some() {
+        if me.test.is_some() {
             bail!("virtual manifests do not specifiy [[test]]");
         }
-        if self.bench.is_some() {
+        if me.bench.is_some() {
             bail!("virtual manifests do not specifiy [[bench]]");
         }
 
         let mut nested_paths = Vec::new();
         let mut warnings = Vec::new();
         let mut deps = Vec::new();
-        let replace = self.replace(&mut Context {
+        let replace = me.replace(&mut Context {
             pkgid: None,
             deps: &mut deps,
             source_id: source_id,
@@ -862,8 +961,8 @@ impl TomlManifest {
             platform: None,
             layout: layout,
         })?;
-        let profiles = build_profiles(&self.profile);
-        let workspace_config = match self.workspace {
+        let profiles = build_profiles(&me.profile);
+        let workspace_config = match me.workspace {
             Some(ref config) => {
                 WorkspaceConfig::Root {
                     members: config.members.clone(),
@@ -1070,7 +1169,7 @@ impl TomlDependency {
     }
 }
 
-#[derive(Default, Deserialize, Debug, Clone)]
+#[derive(Default, Serialize, Deserialize, Debug, Clone)]
 struct TomlTarget {
     name: Option<String>,
 
@@ -1107,8 +1206,16 @@ impl<'de> de::Deserialize<'de> for PathValue {
     }
 }
 
+impl ser::Serialize for PathValue {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+        where S: ser::Serializer,
+    {
+        self.0.serialize(serializer)
+    }
+}
+
 /// Corresponds to a `target` entry, but `TomlTarget` is already used.
-#[derive(Deserialize)]
+#[derive(Serialize, Deserialize)]
 struct TomlPlatform {
     dependencies: Option<HashMap<String, TomlDependency>>,
     #[serde(rename = "build-dependencies")]
index cbcf1468385aef39b61d61c0334dbbaba3ac73de..540c66c57d0f3efe794bc0c677ca772e28ddf490 100644 (file)
@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
 use cargotest::{cargo_process, process};
 use cargotest::support::{project, execs, paths, git, path2url, cargo_exe};
 use flate2::read::GzDecoder;
-use hamcrest::{assert_that, existing_file, contains};
+use hamcrest::{assert_that, existing_file, contains, equal_to};
 use tar::Archive;
 
 #[test]
@@ -62,6 +62,7 @@ src[/]main.rs
         let fname = f.header().path_bytes();
         let fname = &*fname;
         assert!(fname == b"foo-0.0.1/Cargo.toml" ||
+                fname == b"foo-0.0.1/Cargo.toml.orig" ||
                 fname == b"foo-0.0.1/src/main.rs",
                 "unexpected filename: {:?}", f.header().path())
     }
@@ -423,6 +424,7 @@ src[..]main.rs
         let fname = f.header().path_bytes();
         let fname = &*fname;
         assert!(fname == b"nested-0.0.1/Cargo.toml" ||
+                fname == b"nested-0.0.1/Cargo.toml.orig" ||
                 fname == b"nested-0.0.1/src/main.rs",
                 "unexpected filename: {:?}", f.header().path())
     }
@@ -588,3 +590,127 @@ Cargo.toml
 to proceed despite this, pass the `--allow-dirty` flag
 "));
 }
+
+#[test]
+fn generated_manifest() {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [project]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+            exclude = ["*.txt"]
+            license = "MIT"
+            description = "foo"
+
+            [workspace]
+
+            [dependencies]
+            bar = { path = "bar", version = "0.1" }
+        "#)
+        .file("src/main.rs", "")
+        .file("bar/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("bar/src/lib.rs", "");
+
+    assert_that(p.cargo_process("package").arg("--no-verify"),
+                execs().with_status(0));
+
+    let f = File::open(&p.root().join("target/package/foo-0.0.1.crate")).unwrap();
+    let mut rdr = GzDecoder::new(f).unwrap();
+    let mut contents = Vec::new();
+    rdr.read_to_end(&mut contents).unwrap();
+    let mut ar = Archive::new(&contents[..]);
+    let mut entry = ar.entries().unwrap()
+                        .map(|f| f.unwrap())
+                        .find(|e| e.path().unwrap().ends_with("Cargo.toml"))
+                        .unwrap();
+    let mut contents = String::new();
+    entry.read_to_string(&mut contents).unwrap();
+    assert_that(&contents[..], equal_to(
+r#"# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g. crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "foo"
+version = "0.0.1"
+authors = []
+exclude = ["*.txt"]
+description = "foo"
+license = "MIT"
+[dependencies.bar]
+version = "0.1"
+"#));
+}
+
+#[test]
+fn ignore_workspace_specifier() {
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [project]
+            name = "foo"
+            version = "0.0.1"
+            authors = []
+
+            [workspace]
+
+            [dependencies]
+            bar = { path = "bar", version = "0.1" }
+        "#)
+        .file("src/main.rs", "")
+        .file("bar/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+            workspace = ".."
+        "#)
+        .file("bar/src/lib.rs", "");
+    p.build();
+
+    assert_that(p.cargo("package").arg("--no-verify").cwd(p.root().join("bar")),
+                execs().with_status(0));
+
+    let f = File::open(&p.root().join("target/package/bar-0.1.0.crate")).unwrap();
+    let mut rdr = GzDecoder::new(f).unwrap();
+    let mut contents = Vec::new();
+    rdr.read_to_end(&mut contents).unwrap();
+    let mut ar = Archive::new(&contents[..]);
+    let mut entry = ar.entries().unwrap()
+                        .map(|f| f.unwrap())
+                        .find(|e| e.path().unwrap().ends_with("Cargo.toml"))
+                        .unwrap();
+    let mut contents = String::new();
+    entry.read_to_string(&mut contents).unwrap();
+    assert_that(&contents[..], equal_to(
+r#"# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g. crates.io) dependencies
+#
+# If you believe there's an error in this file please file an
+# issue against the rust-lang/cargo repository. If you're
+# editing this file be aware that the upstream Cargo.toml
+# will likely look very different (and much more reasonable)
+
+[package]
+name = "bar"
+version = "0.1.0"
+authors = []
+"#));
+}
index 1e4d71400aad9f681dfc4bdeb9164a8352832cb8..968281f6219a486c8c1f6d6829f24c7d32f75914 100644 (file)
@@ -88,6 +88,7 @@ See [..]
         let fname = file.header().path_bytes();
         let fname = &*fname;
         assert!(fname == b"foo-0.0.1/Cargo.toml" ||
+                fname == b"foo-0.0.1/Cargo.toml.orig" ||
                 fname == b"foo-0.0.1/src/main.rs",
                 "unexpected filename: {:?}", file.header().path());
     }